NLB配下のSMTPサーバー(Postfix)でSendGridのSMTP認証をしてメールを送信してみた
PostfixでSMTPサーバー構築したい
こんにちは!AWS事業本部のおつまみです!
皆さんはPostfixでSMTPサーバー構築したいと思ったことはありますか? 私はあります。
今回案件でNLB + SMTP(Postfix) + SendGrid(SMTP認証)の実装しました。
実装中にハマったポイントもあるので、手順とともにご紹介したいと思います!
なお同じ構成でSMTP認証をGmailで実施した記事はこちらになります。(今回大変参考にさせていただきました。ありがとうございます!)
構成図
今回の構成図です。
今回主役となるMTA(Message Transfer Agent)にPostfixをインストールし、Multi-AZ構成にしてNLBにぶら下がるように構築します。
メール配信の流れは以下です。
- MUA(Message User Agent)
- NLB
- MTA(Message Transfer Agent)
- SendGrid
環境構築
事前準備
まずはメール送信元に使用するドメインおよびSendGridアカウントを準備します。
ドメインはお名前.comやRoute53などお好きな場所からドメインを購入してください。
SendGridはこちらのサイトから無料登録できます。(登録には2~3営業日時間がかかりました。)
SendGrid | クラウドメール配信サービス・メルマガ配信システム
アカウント取得後、SMTP認証に使用するAPIキーを取得します。公式サイトで手順を確認し、取得してください。
APIキーを管理する - ドキュメント | SendGrid
AWSのアクセスキー同様、APIキーは外部に公開されないよう厳重に管理しましょう!
CDK構築初期準備
今回はAWS CDKで環境構築していきます。
利用したCDK versionは2.117.0
です。
CDKを使用したことない方はこちらの公式ドキュメントに従って、環境を整備してから始めましょう。
AWS CDK の開始方法 - AWS Cloud Development Kit (AWS CDK) v2
作成するファイル
今回は1つのスタックファイルでまとめて作成しました。
ハイライトをかけた部分がハマったポイントです。
import * as cdk from "aws-cdk-lib"; import * as s3 from "aws-cdk-lib/aws-s3"; import * as ec2 from "aws-cdk-lib/aws-ec2"; import * as logs from "aws-cdk-lib/aws-logs"; import * as iam from "aws-cdk-lib/aws-iam"; import * as elbv2 from "aws-cdk-lib/aws-elasticloadbalancingv2"; import * as elbv2_tg from 'aws-cdk-lib/aws-elasticloadbalancingv2-targets' import * as fs from "fs"; export class TestMailStack extends cdk.Stack { constructor(scope: cdk.App, id: string, props?: cdk.StackProps) { super(scope, id, props); // Create S3 Bucket for NLB access log const nlbAccessLogBucket = new s3.Bucket(this, "NlbAccessLogBucket", { encryption: s3.BucketEncryption.S3_MANAGED, blockPublicAccess: new s3.BlockPublicAccess({ blockPublicAcls: true, blockPublicPolicy: true, ignorePublicAcls: true, restrictPublicBuckets: true, }), }); console.log(nlbAccessLogBucket.bucketRegionalDomainName); // Create CloudWatch Logs for VPC Flow Logs const flowLogsLogGroup = new logs.LogGroup(this, "FlowLogsLogGroup", { retention: logs.RetentionDays.ONE_WEEK, }); // Create VPC Flow Logs IAM role const flowLogsIamrole = new iam.Role(this, "FlowLogsIamrole", { assumedBy: new iam.ServicePrincipal("vpc-flow-logs.amazonaws.com"), }); // Create SSM IAM role const ssmIamRole = new iam.Role(this, "SsmIamRole", { assumedBy: new iam.ServicePrincipal("ec2.amazonaws.com"), managedPolicies: [ iam.ManagedPolicy.fromAwsManagedPolicyName( "AmazonSSMManagedInstanceCore" ), ], }); // Create VPC Flow Logs IAM Policy const flowLogsIamPolicy = new iam.Policy(this, "FlowLogsIamPolicy", { statements: [ new iam.PolicyStatement({ effect: iam.Effect.ALLOW, actions: ["iam:PassRole"], resources: [flowLogsIamrole.roleArn], }), new iam.PolicyStatement({ effect: iam.Effect.ALLOW, actions: [ "logs:CreateLogStream", "logs:PutLogEvents", "logs:DescribeLogStreams", ], resources: [flowLogsLogGroup.logGroupArn], }), ], }); // Atach VPC Flow Logs IAM Policy flowLogsIamrole.attachInlinePolicy(flowLogsIamPolicy); // Create VPC const vpc = new ec2.Vpc(this, "Vpc", { ipAddresses: ec2.IpAddresses.cidr('172.29.0.0/22'), enableDnsHostnames: true, enableDnsSupport: true, natGateways: 2, maxAzs: 2, subnetConfiguration: [ { name: "Public", subnetType: ec2.SubnetType.PUBLIC, cidrMask: 24 }, { name: "Private", subnetType: ec2.SubnetType.PRIVATE_WITH_EGRESS, cidrMask: 24 }, ], }); // Setting VPC Flow Logs new ec2.CfnFlowLog(this, "FlowLogToLogs", { resourceId: vpc.vpcId, resourceType: "VPC", trafficType: "ALL", deliverLogsPermissionArn: flowLogsIamrole.roleArn, logDestination: flowLogsLogGroup.logGroupArn, logDestinationType: "cloud-watch-logs", logFormat: "${version} ${account-id} ${interface-id} ${srcaddr} ${dstaddr} ${srcport} ${dstport} ${protocol} ${packets} ${bytes} ${start} ${end} ${action} ${log-status} ${vpc-id} ${subnet-id} ${instance-id} ${tcp-flags} ${type} ${pkt-srcaddr} ${pkt-dstaddr} ${region} ${az-id} ${sublocation-type} ${sublocation-id} ${pkt-src-aws-service} ${pkt-dst-aws-service} ${flow-direction} ${traffic-path}", maxAggregationInterval: 60, }); // Security Group for Internal MUA const internalMuaSg = new ec2.SecurityGroup(this, "InternalMuaSg", { allowAllOutbound: true, vpc: vpc, }); // Security Group for NLB const NLBSg = new ec2.SecurityGroup(this, "NLBSg", { allowAllOutbound: true, vpc: vpc, }); NLBSg.addIngressRule( internalMuaSg, ec2.Port.tcp(25), "Allow SMTP for InternalMuaSg" ); // Security Group for Internal MTA const internalMtaSg = new ec2.SecurityGroup(this, "InternalMtaSg", { allowAllOutbound: true, vpc: vpc, }); internalMtaSg.addIngressRule( NLBSg, ec2.Port.tcp(25), "Allow SMTP for NLBSg" ); // Create NLB const nlb = new elbv2.NetworkLoadBalancer(this, "Nlb", { vpc: vpc, vpcSubnets: vpc.selectSubnets({ subnetGroupName: "Private" }), crossZoneEnabled: true, internetFacing: false, securityGroups: [NLBSg], }); nlb.logAccessLogs(nlbAccessLogBucket); // Create NLB Target group const targetGroup = new elbv2.NetworkTargetGroup(this, "TargetGroup", { vpc: vpc, port: 25, targetType: elbv2.TargetType.INSTANCE, }); // Create NLB listener const listener = nlb.addListener("Listener", { port: 25, defaultTargetGroups: [targetGroup], }); // User data for Internal MTA const userDataforInternalMTA = cdk.aws_ec2.UserData.forLinux(); const userDataDefaultScript = fs.readFileSync( "./src/ec2/userDataSettingDefault.sh", "utf8" ); const userDataPostfixMTA = fs.readFileSync( "./src/ec2/userDataSettingPostfixMTA.sh", "utf8" ); const userDataPostfixMUA = fs.readFileSync( "./src/ec2/userDataSettingPostfixMUA.sh", "utf8" ); userDataforInternalMTA.addCommands(userDataDefaultScript); userDataforInternalMTA.addCommands(userDataPostfixMTA); // User data for Internal MUA const userDataforInternalMUA = cdk.aws_ec2.UserData.forLinux(); userDataforInternalMUA.addCommands(userDataDefaultScript); userDataforInternalMUA.addCommands(userDataPostfixMUA); // Create EC2 instance // Internal MTA vpc .selectSubnets({ subnetGroupName: "Private" }) .subnets.forEach((subnet, index) => { const ec2Instance = new ec2.Instance( this, `InternalMtaEc2Instance${index}`, { machineImage: ec2.MachineImage.lookup({ name: "RHEL-9.2.0_HVM-20230905-x86_64-38-Hourly2-GP2", owners: ["309956199498"], }), instanceType: new ec2.InstanceType("t3.micro"), vpc: vpc, keyName: this.node.tryGetContext("key-pair"), role: ssmIamRole, vpcSubnets: vpc.selectSubnets({ subnetGroupName: "Private", availabilityZones: [vpc.availabilityZones[index]], }), securityGroup: internalMtaSg, userData: userDataforInternalMTA, } ); targetGroup.addTarget( new elbv2_tg.InstanceIdTarget(ec2Instance.instanceId, 25) ); }); // MUA new ec2.Instance(this, `MuaEc2Instance`, { machineImage: ec2.MachineImage.lookup({ name: "RHEL-9.2.0_HVM-20230905-x86_64-38-Hourly2-GP2", owners: ["309956199498"], }), instanceType: new ec2.InstanceType("t3.micro"), vpc: vpc, keyName: this.node.tryGetContext("key-pair"), role: ssmIamRole, vpcSubnets: vpc.selectSubnets({ subnetGroupName: "Private", }), securityGroup: internalMuaSg, userData: userDataforInternalMUA, }); } }
ハマったポイント:NLB・MTAに設定するセキュリティグループ
2023/8のアップデートでNLBにセキュリティグループが設定できるようになりました。
これによりターゲットグループとなるMTAでは、NLBのセキュリティグループIDのみ指定すればOKになりました。
このセキュリティグループの設定ができておらず、なかなか通信できない状態になりました。。
UserData
UserDataについてもご紹介します。
今回OSはRHEL9を使用しているため、SSMAgentが事前にインストールされていません。
そのため、UseDataで事前に設定します。なお、AmazonLinux2,2023などを使用する場合はこちらは不要です。
#!/bin/bash # -x to display the command to be executed set -xe # Redirect /var/log/user-data.log and /dev/console exec > >(tee /var/log/user-data.log | logger -t user-data -s 2>/dev/console) 2>&1 # Install Packages token=$(curl -X PUT "http://169.254.169.254/latest/api/token" -H "X-aws-ec2-metadata-token-ttl-seconds: 21600") region_name=$(curl -H "X-aws-ec2-metadata-token: $token" -v http://169.254.169.254/latest/meta-data/placement/availability-zone | sed -e 's/.$//') dnf install -y "https://s3.${region_name}.amazonaws.com/amazon-ssm-${region_name}/latest/linux_amd64/amazon-ssm-agent.rpm" \ unzip \ nvme-cli # SSM Agent systemctl enable amazon-ssm-agent systemctl start amazon-ssm-agent # dnf upgrade dnf upgrade -y # Install AWS CLI curl "https://awscli.amazonaws.com/awscli-exe-linux-x86_64.zip" -o "awscliv2.zip" unzip -q awscliv2.zip sudo ./aws/install rm -rf aws rm -rf awscliv2.zip
MTAにインストールするPostfixの設定もUserDataを使って行います。
ハイライト部分であるyourSendGridApiKey
は事前準備で作成したAPIキーを指定してください。
#!/bin/bash # Check if postfix is installed. sudo dnf install -y postfix # Install the necessary packages. sudo dnf install -y cyrus-sasl-plain jq # Check the current configuration of postfix. postconf -n # Back up the postfix configuration file. sudo cp -a /etc/postfix/main.cf /etc/postfix/main.cf.`date +"%Y%m%d"` # Edit the postfix configuration file. sudo postconf -e "myhostname = internal-mta.o2mami.site" \ "mydomain = o2mami.site" \ "myorigin = \$mydomain" \ "inet_interfaces = all" \ "inet_protocols = ipv4" \ "mydestination = \$myhostname, localhost.\$mydomain, localhost" \ "mynetworks = 172.29.0.0/22, 127.0.0.0/8" \ "home_mailbox = Maildir/" \ "masquerade_domains = \$mydomain" \ "smtpd_banner = \$myhostname ESMTP unknown" \ "relayhost = [smtp.sendgrid.net]:587 " \ "smtp_sasl_auth_enable = yes" \ "smtp_sasl_security_options = noanonymous" \ "smtp_sasl_tls_security_options = noanonymous" \ "smtp_sasl_password_maps = hash:/etc/postfix/sasl_passwd" \ "smtp_sasl_mechanism_filter = plain, login" \ "smtp_use_tls = yes" \ "smtp_tls_security_level = encrypt" \ "smtp_tls_loglevel = 1" \ "smtp_tls_note_starttls_offer = yes" # Create the postfix sasl_passwd file sudo echo "[smtp.sendgrid.net]:587 apikey:yourSendGridApiKey" > /etc/postfix/sasl_passwd # Set file permissions sudo chmod 600 /etc/postfix/sasl_passwd # Reload Postfix configuration sudo postmap /etc/postfix/sasl_passwd # Check the differences of postfix configuration files before and after editing. sudo diff -u /etc/postfix/main.cf.`date +"%Y%m%d"` /etc/postfix/main.cf # Check the postfix configuration file for incorrect descriptions. sudo postfix check # Start postfix. sudo systemctl start postfix # Check the status of postfix. sudo systemctl status postfix # Enable postfix auto-start. sudo systemctl enable postfix # Check the postfix auto-start setting. sudo systemctl is-enabled postfix
Postfixの各設定値については、先人のブログをご確認ください。
NLB配下のPostfixでGmailのSMTP認証をしてメールを送信してみた | DevelopersIO
なおMUAもPostfixを使用して、メール配信を行うのでUserDataを使い、事前にインストールしておきます。
#!/bin/bash # Check if postfix is installed. sudo dnf install -y postfix # Install the necessary packages. sudo dnf install -y sendmail # Back up the postfix configuration file. sudo cp -a /etc/postfix/main.cf /etc/postfix/main.cf.`date +"%Y%m%d"`
AWS CDKでリソースを払い出す
cdk deploy
でリソースを払い出します。
リソース作成後、NLBのDNS名はMUAからメールを送信する際に設定するので控えておきます。
MUAの設定
postconf
コマンドでrelayhost
にNLBのDNS名を指定し、postfixを再起動します。
sh-5.1$ sudo postconf -e "relayhost = TestMa-NlbBC-ErWAk0cwJjcy-73f5dc6943ffaa76.elb.ap-northeast-1.amazonaws.com:25 " sh-5.1$ sudo systemctl restart postfix sh-5.1$ sudo systemctl status postfix ● postfix.service - Postfix Mail Transport Agent Loaded: loaded (/usr/lib/systemd/system/postfix.service; disabled; preset: disabled) Active: active (running) since Fri 2024-01-12 20:25:32 JST; 3min 34s ago (以下略)
これで準備完了です。
メール送信確認
MUAのEC2インスタンスより、自分の社用メールアドレスにメール送信します。
メールログを確認するために、MTAのEC2インスタンス上でそれぞれで以下のコマンドを実行します。egrepでNLBからの疎通確認のログは除外しています。
$ sudo tail -f /var/log/maillog | egrep -v 'disconnect|connect'
次に、MUAからメールを送信します。
Userdateでインストールしたsendmailを使用しました。
sh-5.1$ echo -e 'Subject: TestTitle1\n\nHello!\nWorld' | sendmail -f test@o2mami.site <社用アドレス> sh-5.1$ echo -e 'Subject: TestTitle2\n\nHello!\nWorld' | sendmail -f test@o2mami.site <社用アドレス> sh-5.1$ echo -e 'Subject: TestTitle3\n\nHello!\nWorld' | sendmail -f test@o2mami.site <社用アドレス>
MTAでメールログを確認します。
sh-5.1$ sudo tail -f /var/log/maillog | egrep -v 'disconnect|connect' Jan 12 11:35:04 ip-172-29-3-21 postfix/smtpd[59970]: 4B0FA111A2EF: client=ip-172-29-2-76.ap-northeast-1.compute.internal[172.29.2.76] Jan 12 11:35:04 ip-172-29-3-21 postfix/cleanup[60006]: 4B0FA111A2EF: message-id=<202401121135.40CBZ4rL060360@ip-172-29-2-76.ap-northeast-1.compute.internal> Jan 12 11:35:04 ip-172-29-3-21 postfix/qmgr[59969]: 4B0FA111A2EF: from=<test@o2mami.site>, size=1036, nrcpt=1 (queue active) Jan 12 11:35:05 ip-172-29-3-21 postfix/smtp[60007]: 4B0FA111A2EF: to=<社用アドレス>, relay=smtp.sendgrid.net[52.220.95.193]:587, delay=1.3, delays=0.01/0.06/1.1/0.14, dsn=2.0.0, status=sent (250 Ok: queued as dDjkTtTBSC6CSgCnV7Kv0Q) Jan 12 11:35:05 ip-172-29-3-21 postfix/qmgr[59969]: 4B0FA111A2EF: removed Jan 12 11:35:16 ip-172-29-3-21 postfix/smtpd[59970]: 40452111A2EF: client=ip-172-29-2-76.ap-northeast-1.compute.internal[172.29.2.76] Jan 12 11:35:16 ip-172-29-3-21 postfix/cleanup[60006]: 40452111A2EF: message-id=<202401121135.40CBZGXj060363@ip-172-29-2-76.ap-northeast-1.compute.internal> Jan 12 11:35:16 ip-172-29-3-21 postfix/qmgr[59969]: 40452111A2EF: from=<test@o2mami.site>, size=1036, nrcpt=1 (queue active) Jan 12 11:35:17 ip-172-29-3-21 postfix/smtp[60007]: 40452111A2EF: to=<社用アドレス>, relay=smtp.sendgrid.net[52.220.95.193]:587, delay=0.86, delays=0/0/0.71/0.14, dsn=2.0.0, status=sent (250 Ok: queued as BaHuakO1Qrm9JV1m1XDkQw) Jan 12 11:35:17 ip-172-29-3-21 postfix/qmgr[59969]: 40452111A2EF: removed
sh-5.1$ sudo tail -f /var/log/maillog | egrep -v 'disconnect|connect' Jan 12 11:34:44 ip-172-29-2-210 postfix/smtpd[59971]: C211A13A2: client=ip-172-29-2-76.ap-northeast-1.compute.internal[172.29.2.76] Jan 12 11:34:44 ip-172-29-2-210 postfix/cleanup[59977]: C211A13A2: message-id=<202401121134.40CBYi7S060353@ip-172-29-2-76.ap-northeast-1.compute.internal> Jan 12 11:34:44 ip-172-29-2-210 postfix/qmgr[59893]: C211A13A2: from=<test@o2mami.site>, size=1032, nrcpt=1 (queue active) Jan 12 11:34:46 ip-172-29-2-210 postfix/smtp[59979]: C211A13A2: to=<社用アドレス>, relay=smtp.sendgrid.net[52.220.95.193]:587, delay=1.4, delays=0.02/0.08/1.1/0.14, dsn=2.0.0, status=sent (250Ok: queued as p1BpEJGlR72GALHOCSLX5w) Jan 12 11:34:46 ip-172-29-2-210 postfix/qmgr[59893]: C211A13A2: removed
errorが出ておらず、dsn=2.0.0
となっていることから正しくメールの配送が出来ていそうです。
メールボックスを確認してみます。
以下の通り、3通メールを受信できています。
メールの送信元はMUAで設定したアドレスになっており、「SendGrid」経由であることが確認できました!
さいごに
今回はNLB配下のPostfixでSendGridのSMTP認証をしてメールを送信してみました。
PostfixやSendGridを使用するのは今回が初めてでしたが、設定自体は簡単に行えました!
興味がある方はぜひ自分でSMTPサーバを構築してみてください!
最後までお読みいただきありがとうございました!
どなたかのお役に立てれば幸いです。
以上、おつまみ(@AWS11077)でした!
参考
Postfixでメール送信 - ドキュメント | SendGrid APIキーを管理する - ドキュメント | SendGrid NLB配下のPostfixでGmailのSMTP認証をしてメールを送信してみた | DevelopersIO Amazon Linux 2上のPostfixで宛先ドメイン毎にリレー制御してみた | DevelopersIO